package vip.ifmm.lehouse.commodity.service.api.impl;

import com.alibaba.dubbo.config.annotation.Service;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.transaction.annotation.Transactional;
import vip.ifmm.lehouse.commodity.service.api.CommodityService;
import vip.ifmm.lehouse.commodity.service.dao.CommodityDao;
import vip.ifmm.lehouse.commodity.service.helper.CommodityHelper;
import vip.ifmm.lehouse.commodity.service.utils.PageUtil;
import vip.ifmm.lehouse.common.bo.CommodityBo;
import vip.ifmm.lehouse.common.dto.ServerResponse;
import vip.ifmm.lehouse.common.enums.CommonStatus;
import vip.ifmm.lehouse.common.enums.status.CommodityStatus;
import vip.ifmm.lehouse.common.exception.commodity.*;
import vip.ifmm.lehouse.common.model.Commodity;
import vip.ifmm.lehouse.common.model.CommodityDetail;
import vip.ifmm.lehouse.common.model.CommodityImageLink;
import vip.ifmm.lehouse.common.vo.CommodityVo;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;

/**
 * 商品服务实现类
 *
 * @author Fish
 * <p>Email: fishinlove@163.com</p>
 * <p>created by 2019/06/21 19:47:56</p>
 */
@Service(owner = "fish", mock = "vip.ifmm.lehouse.commodity.service.api.mock.CommodityServiceMock",
        timeout = 12000)
@Slf4j
public class CommodityServiceImpl implements CommodityService {

    // 数据持久层接口
    @Resource
    private CommodityDao commodityDao = null;

    // 默认等待时长，10秒
    private static final int WAIT_TIMEOUT = 10;
    private static final TimeUnit WAIT_TIME_UNIT = TimeUnit.SECONDS;

    @Value("${parallel-insert-num}")
    private int parallelInsertNum = 10000;

    @Value("${core-size}")
    private int coreSize = 8;

    // 信号量，限制数据库写入线程
    private Semaphore semaphore = null;

    // 线程池，用于并发查询数据
    private ThreadPoolExecutor threadPool = null;

    @PostConstruct
    public void init() {
        semaphore = new Semaphore(parallelInsertNum);
        threadPool = new ThreadPoolExecutor(
                coreSize, coreSize * 4, // 最大线程数设置为核心线程数的 4 倍
                30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(parallelInsertNum),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    @Override
    @Transactional
    @CacheEvict(cacheNames = "CommodityService", allEntries = true)
    public ServerResponse<Void> saveCommodity(CommodityBo commodityBo)
            throws CommoditySavedInServiceException {

        // 检查商品信息完整性
        if (!CommodityHelper.isValid(commodityBo)) {
            return ServerResponse.response(CommodityStatus.COMMODITY_INFO_IS_UNCOMPLETED);
        }

        // 执行操作
        // 上面检查完整性是可以大量并发的，而且也可以直接减少一部分不合法的请求来竞争信号量
        try {
            if (!saveCommodityInternal(commodityBo)) {
                return ServerResponse.response(CommodityStatus.COMMODITY_SAVED_FAILED);
            }
        } catch (Exception e) {
            log.error("服务方法中的商品保存失败！商品信息：" + commodityBo, e);

            // 一旦发生任何异常，包括 SQLException 也是，直接回滚数据库
            throw new CommoditySavedInServiceException("商品保存失败！" + commodityBo, e);
        }

        // 到这里说明没有抛出任何异常，也就是保存成功
        return ServerResponse.response(CommodityStatus.SUCCESS);
    }

    @Override
    @Cacheable(cacheNames = "CommodityService")
    public ServerResponse<PageInfo<?>> listCommodities(int pageNum, int pageSize) {
        // 开始分页
        PageUtil.toPage(pageNum, pageSize);
        return ServerResponse.response(CommonStatus.SUCCESS,
                PageUtil.pageInfo(commodityDao.listCommodities()));
    }

    @Override
    @Cacheable(cacheNames = "CommodityService")
    public ServerResponse<PageInfo<?>> listCommoditiesByTypeId(int pageNum,
                                                               int pageSize,
                                                               int typeId) {
        // 开始分页
        PageUtil.toPage(pageNum, pageSize);
        return ServerResponse.response(CommonStatus.SUCCESS,
                PageUtil.pageInfo(commodityDao.listCommoditiesByTypeId(typeId)));
    }

    @Override
    @Cacheable(cacheNames = "CommodityService")
    public ServerResponse<CommodityVo> getCommodity(int commodityId) {

        // 先查询图片，并使用线程池后台执行
        Future<List<CommodityImageLink>> images = imagesOf(commodityId);

        // 同时查询商品其他信息
        CommodityVo commodityVo = commodityDao.getCommodityVo(commodityId);
        if (commodityVo == null) {
            log.info("商品信息：" + commodityVo);
            return ServerResponse.response(CommodityStatus.COMMODITY_NOT_FOUND);
        }

        // 最后将两者结合
        return ServerResponse.response(CommonStatus.SUCCESS, combinedBothOf(commodityVo, images));
    }

    @Override
    @Transactional
    @CacheEvict(cacheNames = "CommodityService", allEntries = true)
    public ServerResponse<Void> updateCommodity(CommodityVo commodityVo) throws CommodityUpdatedInServiceException {

        // 检验商品信息完整性
        if (commodityVo == null) {
            return ServerResponse.response(CommodityStatus.COMMODITY_INFO_IS_UNCOMPLETED);
        }

        // 这边需要执行多条 SQL，但是存在逻辑依赖性，而且事务有可能需要回滚或提交，不适宜用线程池
        // 这边需要说明的是，没有做数据验证，全部推给了数据库做校验，如果发生校验异常，这边会捕获到并向外抛
        // 这样事务就会进行回滚
        try {
            updateCommodityInternal(commodityVo);
            updateCommodityDetail(commodityVo);
            updateCommodityImages(commodityVo);
        } catch (Exception e) {
            throw new CommodityUpdatedInServiceException("商品信息更新失败！商品详情：" + commodityVo, e);
        }

        // 商品信息更新成功
        return ServerResponse.response(CommodityStatus.COMMODITY_UPDATED);
    }

    // 内部保存商品的方法
    // 返回值表示是否正确拿到信号量，如果返回 true，说明操作执行成功，否则执行失败，需要重试
    private boolean saveCommodityInternal(CommodityBo commodityBo) {
        // 原本考虑使用 Future 机制加快 SQL 执行，但是这里存在数据依赖性，没办法执行
        // 由于数据库做了非空处理，所以这里就不做检验了，按道理是需要做的
        // 先插入商品详情，然后插入商品信息，最后插入商品图片
        // 任意环节出错都将抛出一个 RuntimeException 以回滚事务
        log.info("要保存的商品：" + commodityBo);
        try {
            log.info("信号量可用数：" + semaphore.availablePermits());
            if (!semaphore.tryAcquire(WAIT_TIMEOUT, WAIT_TIME_UNIT)) {
                return false;
            }

            // 获取信号量成功
            saveCommodityImages(
                    commodityBo,
                    saveCommodity(commodityBo, saveCommodityDetail(commodityBo))
            );
        } catch (InterruptedException e) {
            log.error("等待信号量被中断！！！", e);
            return false;
        } finally {
            semaphore.release();
        }

        return true;
    }

    // 保存商品详情
    private int saveCommodityDetail(CommodityBo commodityBo) {
        CommodityDetail detail = CommodityHelper.detailFrom(commodityBo);
        log.info("获取到的商品详情：" + detail);
        if (commodityDao.saveCommodityDetail(detail) <= 0) {
            throw new CommodityDetailSavedException();
        }

        // 返回生成的详情 id
        return detail.getCommodityDetailId();
    }

    // 保存商品
    private int saveCommodity(CommodityBo commodityBo, int detailId) {
        Commodity commodity = CommodityHelper.commodityFrom(commodityBo, detailId);
        log.info("获取到的商品是：" + commodity);
        if (commodityDao.saveCommodity(commodity) <= 0) {
            throw new CommoditySavedException("出错商品：" + commodity.getCommodityName());
        }

        // 返回生成的 id
        return commodity.getCommodityId();
    }

    // 保存商品图片
    private void saveCommodityImages(CommodityBo commodityBo, int commodityId) {
        List<CommodityImageLink> images = CommodityHelper.imagesFrom(commodityBo, commodityId);
        log.info("获取到的商品图片是：" + images);
        if (commodityDao.saveCommodityImages(images) <= 0) {
            throw new CommodityImagesSavedException("出错商品图片：" + images);
        }
    }

    // 查询商品图片
    private Future<List<CommodityImageLink>> imagesOf(int commodityId) {
        return threadPool.submit(
                () -> commodityDao.listCommodityImages(commodityId)
        );
    }

    // 将两者融合起来
    private CommodityVo combinedBothOf(CommodityVo commodityVo,
                                       Future<List<CommodityImageLink>> images) {
        try {
            // 使用 Future 等待 10 秒中，否则直接返回
            return commodityVo.setImages(images.get(WAIT_TIMEOUT, WAIT_TIME_UNIT));
        } catch (Exception e) {
            log.error("商品图片查询超时！商品信息：" + commodityVo, e);
        }

        // 等待不了了，直接返回空集合，页面上的图片会显示不出来，其实这里还可以做一层处理
        // 返回特定的图片，比如印有 “程序员笑梗” 的图片集合，作为程序的彩蛋 :)
        return commodityVo.setImages(Collections.emptyList());
    }

    // 内部更新商品的方法
    private void updateCommodityInternal(CommodityVo commodityVo) {
        commodityVo.getCommodity().setCommodityUpdateTime(new Date());
        if (commodityDao.updateCommodity(commodityVo.getCommodity()) <= 0) {
            throw new CommodityUpdatedException("商品更新失败！商品信息：" + commodityVo);
        }
    }

    // 更新商品详情方法
    private void updateCommodityDetail(CommodityVo commodityVo) {
        commodityVo.getCommodityDetail().setCommodityDetailUpdateTime(new Date());
        if (commodityDao.updateCommodityDetail(commodityVo.getCommodityDetail()) <= 0) {
            throw new CommodityDetailUpdatedException("商品详情更新失败！商品信息：" + commodityVo);
        }
    }

    // 更新商品图片方法
    private void updateCommodityImages(CommodityVo commodityVo) {
        // 由于图片可能会很多，所以为了不阻塞，还是使用线程池来进行更新，这样有可能会更新图片失败，但是可能性不大
        threadPool.submit(() -> {
            List<CommodityImageLink> images = commodityVo.getImages();
            for (CommodityImageLink image : images) {
                if (commodityDao.updateCommodityImage(image) <= 0) {
                    log.error("商品图片更新失败！商品图片信息：" + image);
                }
            }
        });
    }
}
